using UnityEngine;
using System.Collections;

[RequireComponent(typeof(Camera))]
[AddComponentMenu("Third Person Camera/Smooth Follow Camera")]

public class SmoothFollowCamera : MonoBehaviour
{
	public Transform target;
	public float distance = 7.0f;
	public float height = 3.0f;
	public float angularSmoothLag = 0.3f;
	public float angularMaxSpeed = 15.0f;
	public float heightSmoothLag = 0.3f;
	public float snapSmoothLag = 0.3f;
	public float snapMaxSpeed = 720.0f;
	public float clampHeadPositionScreenSpace = 0.75f;
	public float lockCameraTimeout = 0.2f;
	
	private Vector3 headOffset = Vector3.zero;
	private Vector3 centerOffset = Vector3.zero;
	private float heightVelocity = 0.0f;
	private float angleVelocity = 0.0f;
	private bool snap = false;
	private ThirdPersonController controller;
	private float targetHeight = 100000.0f;
	
	void Awake()
	{
		DidChangeTarget();	
	}
	
	void DidChangeTarget()
	{
		if(target)
		{
			controller = target.GetComponent<ThirdPersonController>();
		}
		if(controller)
		{
			CharacterController characterController = (CharacterController)target.collider;
			centerOffset = characterController.bounds.center - target.position;
			headOffset = centerOffset;
			headOffset.y = characterController.bounds.max.y - target.position.y;
		}
		else
		{
			Debug.Log("Assign a target to the camera that has a ThirdPersonController attached");
			Cut(target, centerOffset);
		}
	}
	
	void DebugDrawStuff()
	{
		Debug.DrawLine(target.position, target.position + headOffset);
	}
	
	public float AngleDistance(float a, float b)
	{
		a = Mathf.Repeat(a, 360);
		b = Mathf.Repeat(b, 360);
		
		return Mathf.Abs(b-a);
	}
	
	void Apply(Transform dummyTarget, Vector3 dummyCenter)
	{
		//If no target, halt process
		if(!controller)
		{
			return;
		}
		
		Vector3 targetCenter = target.position + centerOffset;
		Vector3 targetHead = target.position + headOffset;
		
		DebugDrawStuff();
		
		//Calculate current and target rotation values
		float originalTargetAngle = target.eulerAngles.y;
		float currentAngle = transform.eulerAngles.y;
		
		//Adjust real target angle when camera is locked
		var targetAngle = originalTargetAngle;
		
		//Snap to target direction with Fire2
		if(Input.GetButton("Fire2"))
		{
			snap = true;
		}
		if(snap)
		{
			//If close to the target, stop snapping
			if(AngleDistance(currentAngle, originalTargetAngle) < 3.0f)
			{
				snap = false;
			}
			
			currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle,ref angleVelocity,snapSmoothLag, snapMaxSpeed);
		}
		//Normal Camera Movement
		else
		{
			if(controller.GetLockCameraTimer() < lockCameraTimeout)
			{
				targetAngle = currentAngle;
			}
			
			//Lock the camera when moving backwards
			if(AngleDistance(currentAngle, targetAngle) > 160f && controller.IsMovingBackwards())
			{
				targetAngle += 180f;
			}
			
			currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle,ref angleVelocity, angularSmoothLag, angularMaxSpeed);
		}
		
		//When jumping only move camera down
		if(controller.IsJumping())
		{
			//Only move the camera if the target is very high
			float newTargetHeight = targetCenter.y + height;
			if((newTargetHeight < targetHeight) || (newTargetHeight - targetHeight > 5))
			{
				targetHeight = targetCenter.y + height;
			}
		}
		//When walking always update target height
		else
		{
			targetHeight = targetCenter.y + height;
		}
		
		//Damp the height
		float currentHeight = transform.position.y;
		currentHeight = Mathf.SmoothDamp(currentHeight, targetHeight,ref heightVelocity, heightSmoothLag);
		
		//Convert the angle into a rotation used to reposition the camera
		Quaternion currentRotation = Quaternion.Euler(0, currentAngle, 0);
		
		//Set the position of the camera on the x/z plane to a distance behind target
		transform.position = targetCenter;
		transform.position += currentRotation * Vector3.back * distance;
		
		//Set the height of the camera
		transform.position = transform.position + new Vector3(0, currentHeight, 0);
		
		//Always look at target
		SetUpRotation(targetCenter, targetHead);
	}
	
	void LateUpdate()
	{
		Apply(transform, Vector3.zero);
	}
	
	void Cut(Transform dummyTarget, Vector3 dummyCenter)
	{
		float oldHeightSmooth = heightSmoothLag;
		float oldSnapMaxSpeed = snapMaxSpeed;
		float oldSnapSmooth = snapSmoothLag;
		
		snapMaxSpeed = 10000f;
		snapSmoothLag = 0.001f;
		heightSmoothLag = 0.001f;
		
		snap = true;
		Apply(transform, Vector3.zero);
		
		heightSmoothLag = oldHeightSmooth;
		snapMaxSpeed = oldSnapMaxSpeed;
		snapSmoothLag = oldSnapSmooth;
	}

	void SetUpRotation(Vector3 centerPos, Vector3 headPos)
	{
		// 1. We first find the rotation around the y axis. Thus he is always centered on the y-axis
		// 2. When grounded we make him be centered
		// 3. When jumping we keep the camera rotation but rotate the camera to get him back into view if his head is above some threshold
		// 4. When landing we smoothly interpolate towards centering him on screen
		
		Vector3 cameraPos = transform.position;
		Vector3 offsetToCenter = centerPos-cameraPos;
		
		//Generate base rotation only around y-axis
		Quaternion yRotation = Quaternion.LookRotation(new Vector3(offsetToCenter.x, 0, offsetToCenter.z));
		
		Vector3 relativeOffset = Vector3.forward*distance + Vector3.down*height;
		transform.rotation = yRotation * Quaternion.LookRotation(relativeOffset);
		
		//Calculate the projected center position and top position in world space
		Ray centerRay = camera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 1f));
		Ray topRay = camera.ViewportPointToRay(new Vector3(0.5f, clampHeadPositionScreenSpace, 1));
		
		Vector3 centerRayPos = centerRay.GetPoint(distance);
		Vector3 topRayPos = topRay.GetPoint(distance);
		
		float centerToTopAngle = Vector3.Angle(centerRay.direction, topRay.direction);
		float heightToAngle = centerToTopAngle / (centerRayPos.y - centerPos.y);
		
		float extraLookAngle = heightToAngle * (centerRayPos.y - centerPos.y);
		if(extraLookAngle < centerToTopAngle)
		{
			extraLookAngle = 0;
		}
		else
		{
			extraLookAngle = extraLookAngle - centerToTopAngle;
			transform.rotation *= Quaternion.Euler(-extraLookAngle, 0, 0);
		}
		
	}
	
	public Vector3 GetCenterOffset()
	{
		return centerOffset;
	}
}